/*
 * Copyright (C) from 2022 The Play Framework Contributors <https://github.com/playframework>, 2011-2021 Lightbend Inc. <https://www.lightbend.com>
 */

package play.utils

import java.io.BufferedInputStream
import java.io.File
import java.net.URL
import java.net.URLConnection
import java.net.URLStreamHandler
import java.nio.file.Files
import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream

import org.specs2.mutable.Specification
import play.api.PlayCoreTestApplication

/**
 * Tests for Resources object
 */
class ResourcesSpec extends Specification {
  import Resources._

  lazy val app             = PlayCoreTestApplication()
  lazy val tmpDir          = createTempDir("resources-", ".tmp")
  lazy val jar             = Files.createTempFile(tmpDir.toPath, "jar-", ".tmp").toFile
  lazy val fileRes         = Files.createTempFile(tmpDir.toPath, "file-", ".tmp").toFile
  lazy val dirRes          = createTempDir("dir-", ".tmp", tmpDir)
  lazy val dirSpacesRes    = createTempDir("dir spaces ", ".tmp", tmpDir)
  lazy val spacesDir       = createTempDir("spaces ", ".tmp", tmpDir)
  lazy val spacesJar       = Files.createTempFile(spacesDir.toPath, "jar-spaces", ".tmp").toFile
  lazy val resourcesDir    = new File(app.classloader.getResource("").getPath)
  lazy val tmpResourcesDir = createTempDir("test-bundle-", ".tmp", resourcesDir)
  lazy val fileBundle      = Files.createTempFile(tmpResourcesDir.toPath, "file-", ".tmp").toFile
  lazy val dirBundle       = createTempDir("dir-", ".tmp", tmpResourcesDir)
  lazy val spacesDirBundle = createTempDir("dir spaces ", ".tmp", tmpResourcesDir)
  lazy val classloader     = app.classloader
  lazy val osgiClassloader = new OsgiClassLoaderSimulator(app.classloader, resourcesDir)

  /* In order to test Resources.isDirectory when the protocol is "bundle://", there are 2 options:
   * a) run the test within an OSGi container (using Pax Exam),
   * b) simulate the behavior of an OSGi class loader (cf. comment in play.utils.Resources). */
  class OsgiClassLoaderSimulator(classloader: ClassLoader, resourcesDir: File) extends ClassLoader {
    override def getResource(name: String): URL = {
      val f    = new File(resourcesDir, name)
      val fURL = f.toURI.toURL
      if (!f.exists) null
      else {
        if (name.last == '/')
          if (f.isDirectory) fURL
          else null
        else fURL
      }
    }
  }

  class BundleStreamHandler extends URLStreamHandler {
    def openConnection(u: URL): URLConnection = throw new Exception("should never happen")
  }

  sequential
  "resources isDirectory" should {
    step {
      createZip(jar, Seq(fileRes, dirRes, dirSpacesRes))
      createZip(spacesJar, Seq(fileRes, dirRes, dirSpacesRes))
    }

    "return true for a directory resource URL with the 'file' protocol" in {
      val url = dirRes.toURI.toURL
      isDirectory(classloader, url) must beTrue
    }

    "return false for a file resource URL with the 'file' protocol" in {
      val url = fileRes.toURI.toURL
      isDirectory(classloader, url) must beFalse
    }

    "return true for a directory resource URL that contains spaces with the 'file' protocol" in {
      val url = dirSpacesRes.toURI.toURL
      isDirectory(classloader, url) must beTrue
    }

    "return true for a directory resource URL with the 'jar' protocol" in {
      val url = new URL("jar", "", createJarUrl(jar, dirRes))
      isDirectory(classloader, url) must beTrue
    }

    "return true for a directory resource URL that contains spaces in the jar path with the 'jar' protocol" in {
      val url = new URL("jar", "", createJarUrl(spacesJar, dirRes))
      isDirectory(classloader, url) must beTrue
    }

    "return true for a directory resource URL that contains spaces in the file path with the 'jar' protocol" in {
      val url = new URL("jar", "", createJarUrl(jar, dirSpacesRes))
      isDirectory(classloader, url) must beTrue
    }

    "return false for a file resource URL with the 'jar' protocol" in {
      val url = new URL("jar", "", createJarUrl(jar, fileRes))
      isDirectory(classloader, url) must beFalse
    }

    "return true for a directory resource URL with the 'bundle' and 'bundleresource' protocols" in {
      val relativeIndex     = dirBundle.getAbsolutePath.indexOf("test-bundle-")
      val dir               = dirBundle.getAbsolutePath.substring(relativeIndex)
      val urlBundle         = new URL("bundle", "325.0", 25, dir, new BundleStreamHandler)
      val urlBundleresource = new URL("bundleresource", "325.0", 25, dir, new BundleStreamHandler)
      (isDirectory(osgiClassloader, urlBundle) must beTrue)
        .and(isDirectory(osgiClassloader, urlBundleresource) must beTrue)
    }

    "return true for a directory resource URL that contains spaces with the 'bundle' and 'bundleresource' protocols" in {
      val relativeIndex     = spacesDirBundle.getAbsolutePath.indexOf("test-bundle-")
      val dir               = spacesDirBundle.getAbsolutePath.substring(relativeIndex)
      val urlBundle         = new URL("bundle", "325.0", 25, dir, new BundleStreamHandler)
      val urlBundleresource = new URL("bundleresource", "325.0", 25, dir, new BundleStreamHandler)
      (isDirectory(osgiClassloader, urlBundle) must beTrue)
        .and(isDirectory(osgiClassloader, urlBundleresource) must beTrue)
    }

    "return false for a file resource URL with the 'bundle' and 'bundleresource' protocols" in {
      val relativeIndex     = fileBundle.getAbsolutePath.indexOf("test-bundle-")
      val file              = fileBundle.getAbsolutePath.substring(relativeIndex)
      val urlBundle         = new URL("bundle", "325.0", 25, file, new BundleStreamHandler)
      val urlBundleresource = new URL("bundleresource", "325.0", 25, file, new BundleStreamHandler)
      (isDirectory(osgiClassloader, urlBundle) must beFalse)
        .and(isDirectory(osgiClassloader, urlBundleresource) must beFalse)
    }

    "return true for a directory resource URL with the 'resource' protocol" in {
      val relativeIndex = dirBundle.getAbsolutePath.indexOf("test-bundle-")
      val dir           = dirBundle.getAbsolutePath.substring(relativeIndex)
      val urlResource   = new URL("resource", "325.0", 25, dir, new BundleStreamHandler)
      isDirectory(osgiClassloader, urlResource) must beTrue
    }

    "return true for a directory resource URL that contains space with the 'resource' protocol" in {
      val relativeIndex = spacesDirBundle.getAbsolutePath.indexOf("test-bundle-")
      val dir           = spacesDirBundle.getAbsolutePath.substring(relativeIndex)
      val urlResource   = new URL("resource", "325.0", 25, dir, new BundleStreamHandler)
      isDirectory(osgiClassloader, urlResource) must beTrue
    }

    "return false for a file resource URL with the 'resource' protocol" in {
      val relativeIndex = fileBundle.getAbsolutePath.indexOf("test-bundle-")
      val file          = fileBundle.getAbsolutePath.substring(relativeIndex)
      val urlResource   = new URL("resource", "325.0", 25, file, new BundleStreamHandler)
      isDirectory(osgiClassloader, urlResource) must beFalse
    }

    "return true for a directory resource URL with the 'zip' protocol" in {
      val url = new URL("zip", "", 0, createZipUrl(jar, dirRes), EmptyURLStreamHandler)
      isDirectory(classloader, url) must beTrue
    }

    "return true for a directory resource URL that contains spaces in the zip path with the 'zip' protocol" in {
      val url = new URL("zip", "", 0, createZipUrl(spacesJar, dirRes), EmptyURLStreamHandler)
      isDirectory(classloader, url) must beTrue
    }

    "return true for a directory resource URL that contains spaces in the file path with the 'zip' protocol" in {
      val url = new URL("zip", "", 0, createZipUrl(jar, dirSpacesRes), EmptyURLStreamHandler)
      isDirectory(classloader, url) must beTrue
    }

    "return false for a file resource URL with the 'zip' protocol" in {
      val url = new URL("zip", "", 0, createZipUrl(jar, fileRes), EmptyURLStreamHandler)
      isDirectory(classloader, url) must beFalse
    }

    "throw an exception for a URL with a protocol other than 'file'/'jar'/'zip' / 'bundle'" in {
      val url = new URL("ftp", "", "/some/path")
      isDirectory(classloader, url) must throwAn[IllegalArgumentException]
    }

    step {
      def delete(file: File): Unit = {
        if (file.isDirectory) file.listFiles().foreach(delete)
        file.delete
      }
      delete(tmpDir)
      delete(tmpResourcesDir)
    }
  }

  object EmptyURLStreamHandler extends URLStreamHandler {

    def openConnection(u: URL) = new URLConnection(u) {
      def connect(): Unit = {}
    }
  }

  private def createJarUrl(jarFile: File, file: File) = {
    s"${jarFile.toURI.toURL}!/${UriEncoding.encodePathSegment(file.getName, "utf-8")}"
  }

  private def createZipUrl(zipFile: File, file: File) = {
    s"zip:${zipFile.toURI.toURL}!/${UriEncoding.encodePathSegment(file.getName, "utf-8")}"
  }

  private def createTempDir(prefix: String, suffix: String, parent: File = null) = {
    val f = if (parent == null) {
      Files.createTempFile(prefix, suffix).toFile
    } else {
      Files.createTempFile(parent.toPath, prefix, suffix).toFile
    }
    f.delete()
    f.mkdir()
    f
  }

  private def createZip(zip: File, files: Seq[File]) = {
    val zipOutputStream = new ZipOutputStream(java.nio.file.Files.newOutputStream(zip.toPath))
    files.foreach(f => addFileToZip(zipOutputStream, f))
    zipOutputStream.close()
  }

  private def addFileToZip(zip: ZipOutputStream, file: File) = {
    val entryName =
      if (file.isDirectory) file.getName + "/"
      else file.getName

    zip.putNextEntry(new ZipEntry(entryName))

    if (!file.isDirectory) {
      val in = new BufferedInputStream(java.nio.file.Files.newInputStream(file.toPath))
      var b  = in.read()
      while (b > -1) {
        zip.write(b)
        b = in.read()
      }
      in.close()
    }

    zip.closeEntry()
  }
}
